<?php
/*
 *
 * @package MoltX
 * @author $Author$
 * @version $Id$
 * @link $URL$
 * @copyright Copyright (c) 2005-2010, Matthew Oltendorf
 * @license http://opensource.org/licenses/gpl-license.php GNU Public License
 *
 */

/*
 * @ignore
 */
if ( ! defined ( 'blink' ) )
{
	// Access denied.
	header ( $_SERVER [ 'SERVER_PROTOCOL' ] . ' 403 Forbidden' );
	echo '403 - Forbidden';

	exit;
}

// Ensure we have the blink_stream class loaded.
include_once blink_system . '/socket/stream.php';

/*
 * Blink server class
 * @package Blink
 *
 * Singleton
 */
final class blink_server
{
	private static	$pid_current = null,
					$pid_file = null,
					$response = null,
					$stream = null,
					$timeout = null;

	/*
	 * Constructor function
	 *
	 * This is private to enforce singleton mode.
	 *
	 * @return null
	 */
	private function __construct ( )
	{
		return null;
	}

	/*
	 * Active check function
	 *
	 * Checks if there's any other scripts that are active on the same stream or
	 * socket. Returns true if it is the active socket, returns false if not.
	 *
	 * @return boolean
	 */
	private static function active ( )
	{
		try
		{
			$pid = file_get_contents ( self :: $pid_file );

			if ( $pid == self :: $pid_current )
			{
				return true;
			}
		}
		catch ( Exception $exception )
		{
			// Ignore.
		}

		return false;
	}

	/*
	 * Connect function
	 *
	 * @return null
	 */
	public static function connect ( $role, $remote )
	{
		$sock = ! $remote ? 'unix://' . blink_storage . '/socket/' . $role . '.sock' : 'tcp://' . $role;

		try
		{
			$resource = stream_socket_client ( $sock );
		}
		catch ( Exception $exception )
		{
			return false;
		}

		return new blink_socket ( $resource );
	}

	/*
	 * Listen function
	 *
	 * Listen on a connection.
	 *
	 * @param string $location the location of the file to use for inter process
	 *		communication, or the address and port to listen on.
	 *
	 * @param boolean $remote whether or not the location is an address, or a
	 *		file for inter process communication.
	 *
	 * @param integer $wait the maximum amount of time for the listener to wait
	 *		for data to be received before calling the function specified by the
	 *		timeout parameter, and checking the socket status. Higher values
	 *		reduce load, while lower values increase responsiveness.
	 *
	 * @param function $response the function that will be called upon when the
	 *		listener receives data. If that function returns false, the listener
	 *		will close the socket, and return.
	 *
	 * @param function $timeout the function that will be called upon when the
	 *		listener doesn't receive data for a certain amount of time. If that
	 *		function returns false, the listener will close the socket, and
	 *		return. Additionally, every time that function is called, the
	 *		listener will check on the socket status. If the socket is no longer
	 *		available, the listener will close the socket and return.
	 *
	 * @return null
	 */
	// @todo: Change to a higher interval for production.
	public static function listen ( $role, $remote = false, $wait = 1, $response = null, $timeout = null )
	{

		$stream = null;

		if ( ! $remote )
		{
			// Development only. @todo: Remove for production.
			blink_console :: log ( 'listen initiating on internal' );

			$file = blink_storage . '/socket/' . $role . '.sock';
			$sock = 'unix://' . $file;

			if ( file_exists ( $file ) )
			{
				// We have to delete this file for the script to be able to listen.
				unlink ( $file );

				sleep ( 1 );
			}
		}
		else
		{
			// Development only. @todo: Remove for production.
			blink_console :: log ( 'listen initiating on external' );

			$sock = 'tcp://' . $role;
		}

		self :: $pid_file = blink_storage . '/socket/' . $role . '.pid';

		if ( file_exists ( self :: $pid_file ) )
		{
			$pid = file_get_contents ( self :: $pid_file );

			if ( $pid > 98 )
			{
				$pid = 0;
			}
			else
			{
				++$pid;
			}
		}
		else
		{
			$pid = 0;
		}

		self :: $pid_current = $pid;

		unset ( $pid );

		$pointer = fopen ( self :: $pid_file, 'w+' );
		blink :: lock ( $pointer );
		fwrite ( $pointer, self :: $pid_current );
		blink :: lock ( $pointer, false );
		fclose ( $pointer );

		// Development only. @todo: Remove for production.
		blink_console :: log ( 'acquired listen lock' );

		$stream = stream_socket_server ( $sock );

		if ( ! $stream )
		{
			// Error.
			throw new Exception ( 'COULD NOT OPEN STREAM SERVER' );
		}

		self :: $stream = $stream;

		self :: $response = $response;
		self :: $timeout = $timeout;

		// Clean up variables as we could be here a very, very long time...
		unset ( $pointer, $role, $sock, $stream );

		// Start the listen loop.
		self :: loop ( $wait );

		fclose ( self :: $stream );

		return null;
	}

	/*
	 * Listen loop function
	 *
	 * Listens for incoming connections and returns with data.
	 *
	 * @param integer $wait how long to wait before timing out, and repeating the loop.
	 *
	 * @return null
	 */
	private static function loop ( $wait )
	{
		// Development only. @todo: Remove for production.
		blink_console :: log ( 'loop started' );

		$client = null;

		for ( ;; )
		{
			try
			{
				$socket = stream_socket_accept ( self :: $stream, $wait );

				if ( ! $socket )
				{
					throw new Exception ( 'TIMEOUT' );
				}

				$client = new blink_socket ( $socket );

				if ( self :: $response && ! call_user_func ( self :: $response, $client ) )
				{
					break;
				}

				$client -> close ( );
				unset ( $client );
			}
			catch ( Exception $exception )
			{
				if ( ( self :: $timeout && ! call_user_func ( self :: $timeout ) ) || ! self :: active ( ) )
				{
					break;
				}
			}

			// Development only. @todo: Remove for production.
			blink_console :: log ( 'looped' );
		}

		// Development only. @todo: Remove for production.
		blink_console :: log ( 'loop ended' );

		if ( $client instanceof blink_socket )
		{
			$client -> close ( );
		}

		return null;
	}
}
